require 'bigdecimal'
require 'active_record/connection_adapters/abstract/schema_definitions'

module ::ArJdbc
  module NuoDB

    def self.column_selector
      [/nuodb/i, lambda { |cfg, col| col.extend(::ArJdbc::NuoDB::ColumnExtensions) }]
    end

    def self.arel2_visitors(config)
      {}.tap { |v| %w(nuodb).each { |a| v[a] = ::Arel::Visitors::NuoDB } }
    end

    # COLUMNS ================================================================

    module ColumnExtensions

      # Maps NuoDB types to logical rails types.
      def simplified_type(field_type)
        case field_type
          when /bit/i then
            :boolean
          when /timestamp/i then
            :timestamp
          when /time/i then
            :time
          when /date/i then
            :date
          else
            super
        end
      end

      def extract_limit(sql_type)
        case sql_type
          when /^smallint/i
            2
          when /^int/i
            4
          when /^bigint/i
            8
          else
            super
        end
      end

    end

    # ADAPTER SUPPORT ========================================================

    ADAPTER_NAME = 'NuoDB'

    NATIVE_DATABASE_TYPES = {
        # generic rails types...
        :binary => {:name => 'binary'},
        :boolean => {:name => 'boolean'},
        :date => {:name => 'date'},
        :datetime => {:name => 'timestamp'},
        :decimal => {:name => 'decimal'},
        :float => {:name => 'float', :limit => 8},
        :integer => {:name => 'integer'},
        :primary_key => 'int not null generated by default as identity primary key',
        :string => {:name => 'varchar', :limit => 255},
        :text => {:name => 'varchar', :limit => 255},
        :time => {:name => 'time'},
        :timestamp => {:name => 'timestamp'},
        # nuodb specific types...
        :char => {:name => 'char'},
        :numeric => {:name => 'numeric(20)'},
    }

    # Maps logical rails types to NuoDB types.
    def native_database_types
      NATIVE_DATABASE_TYPES
    end

    def adapter_name
      ADAPTER_NAME
    end

    def supports_savepoints?
      true
    end

    def supports_ddl_transactions?
      true
    end

    def supports_index_sort_order?
      true
    end

    def create_savepoint
      execute("SAVEPOINT #{current_savepoint_name}")
    end

    def rollback_to_savepoint
      execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
    end

    def release_savepoint
      execute("RELEASE SAVEPOINT #{current_savepoint_name}")
    end

    def modify_types(tp)
      tp[:primary_key] = 'int not null generated always primary key'
      tp[:boolean] = {:name => 'boolean'}
      tp[:date] = {:name => 'date', :limit => nil}
      tp[:datetime] = {:name => 'timestamp', :limit => nil}
      tp[:decimal] = {:name => 'decimal'}
      tp[:integer] = {:name => 'int', :limit => 4}
      tp[:string] = {:name => 'string'}
      tp[:time] = {:name => 'time', :limit => nil}
      tp[:timestamp] = {:name => 'timestamp', :limit => nil}
      tp
    end

    protected

    def translate_exception(exception, message)
      # future translations here...
      super
    end

    public

    # QUOTING ================================================================

    def quote(value, column = nil)
      case value
        when TrueClass, FalseClass
          value.to_s
        else
          super
      end
    end

    def quote_column_name(name)
      "`#{name.to_s.gsub('`', '``')}`"
    end

    def quote_table_name(name)
      quote_column_name(name).gsub('.', '`.`')
    end

    def type_cast(value)
      return super unless value == true || value == false
      value ? true : false
    end

    def quoted_true
      "'true'"
    end

    def quoted_false
      "'false'"
    end

    def quoted_date(value)
      if value.acts_like?(:time)
        zone_conversion_method = :getutc
        if value.respond_to?(zone_conversion_method)
          value = value.send(zone_conversion_method)
        end
      end
      value.to_s(:db)
    end

    # COMPATIBILITY ==========================================================

    # SCHEMA STATEMENTS ======================================================

    def tables(name = nil)
      #puts "called columns"
      super
    end

    def indexes(table_name, name = nil, schema_name = nil)
      #puts "called columns"
      super
    end

    def columns(table_name, name=nil)
      #puts "called columns"
      @connection.columns_internal(table_name.to_s, name, nuodb_schema)
    end

    # maps logical rails types to nuodb-specific data types.
    def type_to_sql(type, limit = nil, precision = nil, scale = nil)
      case type.to_s
        when 'integer'
          return 'integer' unless limit
          case limit
            when 1..2
              'smallint'
            when 3..4
              'integer'
            when 5..8
              'bigint'
            else
              raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
          end
        when 'timestamp'
          column_type_sql = 'timestamp'
          unless precision.nil?
            case precision
              when 0..9
                column_type_sql << "(#{precision})"
              else
                nil
            end
          end
          column_type_sql
        when 'time'
          column_type_sql = 'time'
          unless precision.nil?
            case precision
              when 0..9
                column_type_sql << "(#{precision})"
              else
                nil
            end
          end
          column_type_sql
        else
          super
      end
    end

    def rename_column(table_name, column_name, new_column_name)
      raise NotImplementedError, 'rename_column is not implemented'
    end

    def rename_table(table_name, new_name)
      raise NotImplementedError, 'rename_table is not implemented'
    end

    def recreate_database(name, options = {}) #:nodoc:
      drop_database(name)
      create_database(name, options)
    end

    def create_database(name, options = {}) #:nodoc:
      $stderr.puts 'create_database is not implemented'
    end

    def drop_database(name) #:nodoc:
      $stderr.puts 'drop_database is not implemented'
    end

    # DATABASE STATEMENTS ====================================================

    LOST_CONNECTION_ERROR_MESSAGES = [
        'End of stream reached',
        'Broken pipe',
        'Connection reset'
    ]

    # Monkey patch the execute method as reconnect is broken in the underlying
    # Rails infrastructure; see these bug numbers and references:
    #
    # - https://github.com/jruby/activerecord-jdbc-adapter/issues/232
    # - https://github.com/jruby/activerecord-jdbc-adapter/issues/237

    def execute(sql, name = nil, binds = [])
      tries ||= 2
      super(sql, name, binds)
    rescue ActiveRecord::StatementInvalid, ActiveRecord::JDBCError => exception
      if LOST_CONNECTION_ERROR_MESSAGES.any? { |msg| exception.message =~ /#{msg}/ }
        if open_transactions == 0
          reconnect!
          retry unless (tries -= 1).zero?
        end
      end
      raise
    end

    def begin_db_transaction
      tries ||= 2
      super
    rescue ActiveRecord::StatementInvalid, ActiveRecord::JDBCError => exception
      if LOST_CONNECTION_ERROR_MESSAGES.any? { |msg| exception.message =~ /#{msg}/ }
        reconnect!
        retry unless (tries -= 1).zero?
      end
      raise
    end

    def commit_db_transaction
      super
    rescue ActiveRecord::StatementInvalid, ActiveRecord::JDBCError => exception
      if LOST_CONNECTION_ERROR_MESSAGES.any? { |msg| exception.message =~ /#{msg}/ }
        reconnect!
      end
      raise
    end

    def rollback_db_transaction
      super
    rescue ActiveRecord::StatementInvalid, ActiveRecord::JDBCError => exception
      if LOST_CONNECTION_ERROR_MESSAGES.any? { |msg| exception.message =~ /#{msg}/ }
        reconnect!
      else
        raise
      end
    end

    # CONNECTION POOL ========================================================

    def primary_keys(table)
      @connection.primary_keys(qualify_table(table))
    end

    private

    def qualify_table(table)
      if (table.include? '.') || @config[:schema].blank?
        table
      else
        nuodb_schema + '.' + table
      end
    end

    def nuodb_schema
      config[:schema] || ''
    end

  end

end

module ActiveRecord::ConnectionAdapters

  class NuoDBColumn < JdbcColumn

    include ArJdbc::NuoDB::ColumnExtensions

    def initialize(name, *args)
      if Hash === name
        super
      else
        super(nil, name, *args)
      end
    end

    def call_discovered_column_callbacks(*)
    end
  end

  class NuoDBAdapter < JdbcAdapter

    include ArJdbc::NuoDB

    def initialize(*args)
      super
    end

    def jdbc_column_class
      ActiveRecord::ConnectionAdapters::NuoDBColumn
    end

  end

end